﻿namespace LuckyAssetManagerContrib.Web
{
	using System;
	using System.Collections.Specialized;
	using System.IO.Compression;
	using System.Runtime.Caching;
	using System.Security.Cryptography;
	using System.Text;
	using System.Web;

	using Lucky.AssetManager;

	using Configuration;
	using FileCache;
	using Resources;

	/// <summary>
	/// HTTP-handler, responsible for text output 
	/// of processed asset
	/// </summary>
	public sealed class AssetsHandler : IHttpHandler
	{
		/// <summary>
		/// Memory cache
		/// </summary>
		private readonly ObjectCache _memCache;

		/// <summary>
		/// File cache manager
		/// </summary>
		private readonly AssetFileCacheManager _fileCacheManager;

		/// <summary>
		/// Configuration settings of HTTP-handler
		/// </summary>
		private readonly AssetsHandlerElement _config;

		public bool IsReusable
		{
			get { return false; }
		}


		public AssetsHandler()
			: this(AssetManagerContext.Configuration.CacheFactory.GetCache(), 
				AssetManagerContext.FileCacheManager,
				AssetManagerContext.ContribConfiguration.AssetsHandler) 
		{ }

		public AssetsHandler(ObjectCache memCache, AssetFileCacheManager fileCacheManager, 
			AssetsHandlerElement config)
		{
			_memCache = memCache;
			_fileCacheManager = fileCacheManager;
			_config = config;
		}


		public void ProcessRequest(HttpContext context)
		{
			ProcessRequest(new HttpContextWrapper(context));
		}

		public void ProcessRequest(HttpContextBase context)
		{
			var response = context.Response;
			var request = context.Request;

			bool useLastModified = _config.UseLastModifiedHeader;
			bool useETag = _config.UseETagHeader;

			// Determine requested asset type
			AssetType assetType = GetAssetType(request.QueryString);

			// Get and verify cache item key
			string cacheItemKey = GetCacheItemKey(request.QueryString);
			if (!CheckCacheItemExist(cacheItemKey))
			{
				// If cache item does not exist, then notify 
				// browser that requested asset was not found
				ThrowHttpNotFoundException(response);
				return;
			}

			// Get date and time of last modification requested asset
			DateTime lastModifyTime = GetAssetLastModifyTime(cacheItemKey);

			// Generate ETag value
			string eTag = GenerateAssetETag(cacheItemKey, lastModifyTime);

			// Check whether requested asset has modified
			bool lastModifiedChanged = !useLastModified || IsLastModifiedHeaderChanged(request, lastModifyTime);
			bool eTagChanged = !useETag || IsETagHeaderChanged(request, eTag);

			if (!(lastModifiedChanged && eTagChanged))
			{
				// Requested asset has not changed, so return 304 HTTP-status
				response.StatusCode = 304;
				response.StatusDescription = "Not Modified";

				// Set HTTP-header "Content-Length" is zero, so that 
				// client did not wait for data, but also kept 
				// connection open for other requests
				response.AddHeader("Content-Length", "0");

				response.End();

				return;
			}

			// Trying to read data from cache
			string content = GetAssetContentFromCache(cacheItemKey);
			if (content == null)
			{
				// If cache item is empty, then notify 
				// browser that requested asset was not found
				ThrowHttpNotFoundException(response);
				return;
			}
			
			switch (assetType)
			{
				case AssetType.Css:
					response.ContentType = @"text/css";
					break;
				case AssetType.Javascript:
					response.ContentType = @"application/x-javascript";
					break;
			}

			// Add a special HTTP-headers to ensure that 
			// asset caching in browsers
			var clientCache = response.Cache;
			var cacheDuration = TimeSpan.FromDays(_config.ClientCacheDurationInDays);

			clientCache.SetCacheability(HttpCacheability.Public);
			clientCache.SetExpires(DateTime.Now.Add(cacheDuration));
			clientCache.SetValidUntilExpires(true);
			clientCache.SetNoServerCaching();
			clientCache.SetMaxAge(cacheDuration);
			clientCache.AppendCacheExtension("must-revalidate");
			if (useLastModified)
			{
				clientCache.VaryByHeaders["If-Modified-Since"] = true;
				clientCache.SetLastModified(lastModifyTime);
			}
			if (useETag)
			{
				clientCache.VaryByHeaders["If-None-Match"] = true;
				clientCache.SetETag(eTag);
			}

			if (_config.EnableCompression)
			{
				// Compress asset by GZIP/Deflate
				TryCompressAsset(context);
			}

			// Output text content of asset
			response.Write(content);
			response.End();
		}

		/// <summary>
		/// Gets key of cache item, which 
		/// contains text content of processed asset
		/// </summary>
		/// <param name="queryStringParams">Query string parameters</param>
		/// <returns>Cache item key</returns>
		private static string GetCacheItemKey(NameValueCollection queryStringParams)
		{
			var key = queryStringParams["key"];
			if (string.IsNullOrWhiteSpace(key))
			{
				throw new ArgumentException(String.Format(Strings.AssetsHandler_RequestParameterIsEmpty, "key"));
			}
			return key;
		}

		/// <summary>
		/// Gets asset type
		/// </summary>
		/// <param name="queryStringParams">Query string parameters</param>
		/// <returns>Asset type</returns>
		private static AssetType GetAssetType(NameValueCollection queryStringParams)
		{
			AssetType assetType;
			string assetTypeCode = queryStringParams["type"];

			if (string.IsNullOrWhiteSpace(assetTypeCode))
			{
				throw new ArgumentException(String.Format(Strings.AssetsHandler_RequestParameterIsEmpty, "type"));
			}

			if (!Enum.TryParse(assetTypeCode, true, out assetType))
			{
				throw new ArgumentException(Strings.AssetsHandler_RequestParameterIvalidType);
			}

			return assetType;
		}

		/// <summary>
		/// Checks existance of cache item
		/// </summary>
		/// <param name="cacheItemKey">Cache item key</param>
		/// <returns>Result of checking (true – exist; false – not exist)</returns>
		private bool CheckCacheItemExist(string cacheItemKey)
		{
			_fileCacheManager.UpdateItemLastAccessTime(cacheItemKey);

			bool result = _memCache.Contains(cacheItemKey) || _fileCacheManager.ContainsItem(cacheItemKey);

			return result;
		}

		/// <summary>
		/// Gets text content of processed asset from cache
		/// </summary>
		/// <param name="cacheItemKey">Cache item key</param>
		/// <returns>Text content of processed asset</returns>
		private string GetAssetContentFromCache(string cacheItemKey)
		{
			_fileCacheManager.UpdateItemLastAccessTime(cacheItemKey);

			object memCacheItem = _memCache.Get(cacheItemKey);
			if (memCacheItem != null)
			{
				return (string)memCacheItem;
			}

			var fileCacheItem = _fileCacheManager.GetItem(cacheItemKey);
			if (fileCacheItem != null)
			{
				return fileCacheItem.ReadTempFileContent();
			}

			return null;
		}

		/// <summary>
		/// Throw 404 error (page not found)
		/// </summary>
		/// <param name="response">HttpResponse object</param>
		private static void ThrowHttpNotFoundException(HttpResponseBase response)
		{
			response.Clear();
			response.StatusCode = 404;
			response.End();
		}

		/// <summary>
		/// Gets date and time of last modification processed asset
		/// </summary>
		/// <param name="cacheItemKey">Cache item key</param>
		/// <returns>Date and time of last modification 
		/// files associated with processed asset</returns>
		private DateTime GetAssetLastModifyTime(string cacheItemKey)
		{
			string lastModifyDateKey = String.Format(
				Constants.ProcessedAssetLastModifyTimeCacheItemKeyPattern,
				cacheItemKey);

			var memCacheItem = _memCache.Get(lastModifyDateKey);
			if (memCacheItem != null)
			{
				return (DateTime) memCacheItem;
			}

			var fileCacheItem = _fileCacheManager.GetItem(cacheItemKey);
			if (fileCacheItem != null)
			{
				return fileCacheItem.LastModifyTime;
			}

			return DateTime.MinValue;
		}

		/// <summary>
		/// Generates value for HTTP-header "ETag" based on 
		/// information about processed asset
		/// </summary>
		/// <param name="cacheItemKey">Cache item key</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		/// <returns>ETag value</returns>
		private static string GenerateAssetETag(string cacheItemKey, DateTime lastModifyTime)
		{
			string hash;
			int numericKey;

			if (int.TryParse(cacheItemKey, out numericKey))
			{
				hash = String.Concat(numericKey.ToString("x"), Utils.ConvertDateTimeToHashString(lastModifyTime));
			}
			else
			{
				string uniqueId = String.Concat(cacheItemKey, lastModifyTime);

				Encoder stringEncoder = Encoding.UTF8.GetEncoder();
				int byteCount = stringEncoder.GetByteCount(uniqueId.ToCharArray(), 0, uniqueId.Length, true);
				var bytes = new Byte[byteCount];

				stringEncoder.GetBytes(uniqueId.ToCharArray(), 0, uniqueId.Length, bytes, 0, true);

				MD5 md5 = MD5.Create();
				hash = BitConverter
					.ToString(md5.ComputeHash(bytes))
					.Replace("-", String.Empty)
					.ToLower()
					;
			}

			string eTag = String.Format("\"{0}\"", hash);

			return eTag;
		}

		/// <summary>
		/// Checks actuality of data in browser cache using 
		/// HTTP-header "Last-Modified"
		/// </summary>
		/// <param name="request">HttpRequest object</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		/// <returns>Result of checking (true – data has changed; 
		/// false – has not changed)</returns>
		private static bool IsLastModifiedHeaderChanged(HttpRequestBase request, DateTime lastModifyTime)
		{
			bool fileDateModified = true;
			DateTime modifiedSince = DateTime.MinValue;

			string ifModifiedSince = request.Headers["If-Modified-Since"];
			if (!String.IsNullOrWhiteSpace(ifModifiedSince))
			{
				if (!DateTime.TryParse(ifModifiedSince, out modifiedSince))
				{
					modifiedSince = DateTime.MinValue;
				}
			}

			if (modifiedSince != DateTime.MinValue)
			{
				TimeSpan modifyDiff = lastModifyTime - modifiedSince;
				fileDateModified = modifyDiff > TimeSpan.FromSeconds(1);
			}

			return fileDateModified;
		}

		/// <summary>
		/// Checks actuality of data in browser cache using 
		/// HTTP-header "ETag"
		/// </summary>
		/// <param name="request">HttpRequest object</param>
		/// <param name="eTag">ETag value</param>
		/// <returns>Result of checking (true – data has changed; 
		/// false – has not changed)</returns>
		private static bool IsETagHeaderChanged(HttpRequestBase request, string eTag)
		{
			bool eTagChanged = true;
			string ifNoneMatch = request.Headers["If-None-Match"];

			if (!String.IsNullOrWhiteSpace(ifNoneMatch))
			{
				eTagChanged = !String.Equals(ifNoneMatch, eTag, StringComparison.InvariantCulture);
			}

			return eTagChanged;
		}

		/// <summary>
		/// Compresses processed asset, using GZIP or Deflate 
		/// (if there is support in browser)
		/// </summary>
		/// <param name="context">HttpContext object</param>
		public void TryCompressAsset(HttpContextBase context)
		{
			var response = context.Response;
			string acceptEncoding = context.Request.Headers["Accept-Encoding"];

			if (acceptEncoding != null)
			{
				if (acceptEncoding.Contains("gzip"))
				{
					response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
					response.AppendHeader("Content-Encoding", "gzip");
				}
				else if (acceptEncoding.Contains("deflate"))
				{
					response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
					response.AppendHeader("Content-Encoding", "deflate");
				}
			}
		}
	}
}